// Copyright (c) 2011 Zeex
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include <unordered_map>

#include "amxdbgw.h"
#include "amxnamefinder.h"
#include "amxstacktrace.h"
#include "crashdetector.h"
#include "fileutils.h"
#include "jump.h"
#include "logprintf.h"
#include "plugincommon.h"
#include "settings.h"

#include "amx/amx.h"
#include "amx/amxaux.h"
#include "amx/amxdbg.h"

#define AMX_EXEC_GDK (-10) // for compatibility with GDK

static std::unordered_map<AMX*, std::shared_ptr<AMX_DBG>> amxmap;

static std::unordered_map<AMX*, AMX_DEBUG> amxDebugHooks;
static std::unordered_map<AMX*, AMX_CALLBACK> amxCallbacks;

static struct NativeCallInfo {
    AMX  *amx;
    char *name;
    bool returned;
} lastNative;

static int AMXAPI AmxCallback(AMX *amx, cell index, cell *result, cell *params) {
    auto hdr = reinterpret_cast<AMX_HEADER*>(amx->base);
    auto nativeTable = reinterpret_cast<AMX_FUNCSTUBNT*>(
        hdr->natives + reinterpret_cast<int32_t>(amx->base));

    // Store some info about this native call in global variable
    ::lastNative.amx = amx;
    ::lastNative.name = reinterpret_cast<char*>(nativeTable[index].nameofs +
        reinterpret_cast<int32_t>(hdr));
    ::lastNative.returned = false;

    // Reset error
    amx->error = AMX_ERR_NONE;

    // Call any previously set debug hook (amx_Callback by default)
    AMX_CALLBACK oldCallback = ::amxCallbacks[amx];
    if (oldCallback != 0) {
        oldCallback(amx, index, result, params);
    }

    ::lastNative.returned = true;

    if (SettingsHolder::GetInstance()->GetSettings().catchNativeErrors) {
        // Check if the AMX_ERR_NATIVE error is set
        if (amx->error == AMX_ERR_NATIVE) {
            auto amxname = AmxNameFinder::GetInstance()->GetAmxName(amx);
            auto amxdbg = ::amxmap[amx].get();
            if (amxdbg != 0) {
                logprintf("Script[%s]: Native function %s called at line %ld in %s has failed.",
                    amxname.c_str(), ::lastNative.name, LookupLine(amxdbg, amx->cip),
                    fileutils::GetBaseName(LookupFile(amxdbg, amx->cip)).c_str());
                PrintAmxStackTrace(amx, amxdbg, amxname);
            } else {
                logprintf("Script[%s]: Native function %s called at address %p has failed.",
                    amxname.c_str(), ::lastNative.name, amx->cip);
            }
            if (SettingsHolder::GetInstance()->GetSettings().exitOnNativeError) {
                exit(AMX_ERR_NATIVE);
            }
        }
    }

    // Reset error
    amx->error = AMX_ERR_NONE;

    return AMX_ERR_NONE;
}

// We force AMX to advance amx->cip by setting a debug hook
static int AMXAPI AmxDebugHook(AMX *amx) {
    AMX_DEBUG oldDebug = ::amxDebugHooks[amx];
    if (oldDebug != 0) {
        return oldDebug(amx);
    }
    return AMX_ERR_NONE;
}

static void PrintCrashReport() {
    // Check if the last native call succeeded
    if (!::lastNative.returned) {
        auto amx = ::lastNative.amx;
        auto amxdbg = ::amxmap[amx].get();
        auto amxname = AmxNameFinder::GetInstance()->GetAmxName(amx);
        if (amxdbg != 0) {
            logprintf("The server has been crashed by %s at line %ld in %s.", ::lastNative.name,
                LookupLine(amxdbg, amx->cip), fileutils::GetBaseName(LookupFile(amxdbg, amx->cip)).c_str());
            PrintAmxStackTrace(amx, amxdbg, amxname);
        } else {
            logprintf("The server has been crashed by %s at address %p in %s.",
                ::lastNative.name, amx->cip, amxname.c_str());
        }
    } else {
        // Server/plugin internal error, we don't know the reason
        logprintf("The server has crashed due to an unexpected error.");
    }
}

static int AMXAPI AmxExec(AMX *amx, cell *retval, int index) {
    return amx_Exec(amx, retval, index);
}

extern "C" void AMXAPI crashdetect_handle_runtime_error(AMX *amx, int index, int error) {
    if (error != AMX_ERR_NONE) {
        if (error == AMX_ERR_INDEX && index == AMX_EXEC_GDK) {
            // Do not issue an error on invalid index when called by GDK.
            error = AMX_ERR_NONE;
        } else {
            char entryPoint[33];
            // Get entry point name
            if (index == AMX_EXEC_MAIN) {
                strcpy(entryPoint, "main");
            } else {
                if (index < 0 || amx_GetPublic(amx, index, entryPoint) != AMX_ERR_NONE) {
                    strcpy(entryPoint, "<unknown function>");
                }
            }
            auto amxname = AmxNameFinder::GetInstance()->GetAmxName(amx);
            auto amxdbg = ::amxmap[amx].get();
            if (amxdbg == 0) {
                logprintf("Script[%s]: During execution of %s:",
                    amxname.c_str(), entryPoint);
                logprintf("Script[%s]: Run time error %d: \"%s\"",
                    amxname.c_str(), error, aux_StrError(error));
            } else {
                logprintf("Script[%s]: Run time error %d: \"%s\"",
                    amxname.c_str(), error, aux_StrError(error));
                PrintAmxStackTrace(amx, amxdbg, amxname);
            }
            if (SettingsHolder::GetInstance()->GetSettings().exitOnRuntimeError) {
                exit(error);
            }
        }
    }
}

namespace natives {

    // native PrintStackTrace();
    cell AMX_NATIVE_CALL PrintStackTrace(AMX *amx, cell *params)
    {
        auto amxdbg = ::amxmap[amx].get();;
        if (amxdbg != 0) {
            auto amxname = AmxNameFinder::GetInstance()->GetAmxName(amx);
            PrintAmxStackTrace(amx, amxdbg, amxname);
            return 1;
        }
        return 0;
    }

    const AMX_NATIVE_INFO all[] = {
        {"PrintStackTrace", PrintStackTrace},
        {0,                 0}
    };

} // namespace natives

PLUGIN_EXPORT unsigned int PLUGIN_CALL Supports() {
    return SUPPORTS_VERSION | SUPPORTS_AMX_NATIVES;
}

extern void *pAMXFunctions;

PLUGIN_EXPORT bool PLUGIN_CALL Load(void **ppPluginData) {
    void *pAMXFunctions = ppPluginData[PLUGIN_DATA_AMX_EXPORTS];
    logprintf = (logprintf_t)ppPluginData[PLUGIN_DATA_LOGPRINTF];

    CrashDetector::GetInstance()->SetCrashHandler(PrintCrashReport);
    SettingsHolder::GetInstance()->ReadSettings("plugins/crashdetect.cfg");

    if (SettingsHolder::GetInstance()->GetSettings().catchRuntimeErrors) {
        // Hook amx_Exec to track scripting runtime errors
        SetJump(((void**)pAMXFunctions)[PLUGIN_AMX_EXPORT_Exec], (void*)AmxExec);
    }

    AmxNameFinder::GetInstance()->AddSearchDir("gamemodes");
    AmxNameFinder::GetInstance()->AddSearchDir("filterscripts");


    return true;
}

PLUGIN_EXPORT void PLUGIN_CALL Unload() {
    return;
}

PLUGIN_EXPORT int PLUGIN_CALL AmxLoad(AMX *amx) {
    // Try to determine .amx file name.
    AmxNameFinder::GetInstance()->UpdateCache();
    auto amxname = AmxNameFinder::GetInstance()->GetAmxName(amx);
    //logprintf(amxname.c_str()); // Ensure amxname is OK

    uint16_t flags;
    amx_Flags(amx, &flags);
    // Check if the script has symbolic debug information.
    if ((flags & AMX_FLAG_DEBUG) != 0) {
        FILE* fp = std::fopen(amxname.c_str(), "rb");
        if (fp != 0) {
            auto tmpamxdbg = new AMX_DBG;
            if (dbg_LoadInfo(tmpamxdbg, fp) != AMX_ERR_NONE) {
                delete tmpamxdbg;
                logprintf("crashdetect: Failed to load debug symbols from %s", amxname.c_str());
            } else {
                ::amxmap[amx].reset(tmpamxdbg);
                logprintf("crashdetect: Loaded symbols from %s", amxname.c_str());
            }
            std::fclose(fp);
        } else {
            logprintf("crashdetect: Cannot open file %s", amxname.c_str());
        }
    }

    ::amxDebugHooks[amx] = amx->debug;
    amx_SetDebugHook(amx, AmxDebugHook);

    // Disable overriding of SYSREQ.C opcodes by SYSREQ.D
    amx->sysreq_d = 0;

    ::amxCallbacks[amx] = amx->callback;
    amx_SetCallback(amx, AmxCallback);

    //return amx_Register(amx, natives::all, -1);
    return AMX_ERR_NONE;
}

PLUGIN_EXPORT int PLUGIN_CALL AmxUnload(AMX *amx) {
    auto amxdbg = ::amxmap[amx].get();
    if (amxdbg != 0) {
        dbg_FreeInfo(amxdbg);
    }
    ::amxmap.erase(amx); // amxdbg is freed automatically
    return AMX_ERR_NONE;
}
